import datetime
from itertools import groupby
from collections import defaultdict
from math import ceil

from markupsafe import Markup
from odoo import fields, models, api, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare, float_is_zero


class ShippingInstructionsLine(models.Model):
    _name = 'ship.instructions.line'

    name = fields.Char(string='Name')
    ship_instructions_id = fields.Many2one('ship.instructions', ondelete='cascade')
    sale_order_id = fields.Many2one('sale.order', string='Sales')
    sale_order_line_id = fields.Many2one('sale.order.line')
    po_name = fields.Char(string='PO', related='sale_order_line_id.po')
    pn = fields.Char(string='PN', related='sale_order_line_id.pn')
    pn_desc = fields.Char(string='PN Desc', related='sale_order_line_id.pn_des')
    pn_rev = fields.Char(string='PN Rev.', related='sale_order_line_id.pn_rev')
    end_po = fields.Char(string='End PO', related='sale_order_id.end_po')
    end_pn = fields.Char(string='End PN', related='sale_order_id.end_pn')
    end_pn_rev = fields.Char(string='End PN Rev', related='sale_order_id.end_pn_rev')
    instructed_qty = fields.Integer(string="Instructed Qty")
    product_id = fields.Many2one('product.product', string='Product', related='sale_order_line_id.product_id')
    # 订单数
    product_uom_qty = fields.Float(string='Quantity', digits='Product Unit of Measure', default=0,
                                   related="sale_order_line_id.product_uom_qty")
    delivered_qty = fields.Float(string='Delivered', related='sale_order_line_id.qty_delivered', digits=(16, 0))
    qty_to_deliver = fields.Float(string='Qty To Deliver', related='sale_order_line_id.qty_to_deliver', digits=(16, 0))
    unassigned_qty = fields.Integer(string="UN Instruct Qty", default=lambda self: self.delivered_qty - self.instructed_qty)
    # 指派数
    delivery_qty = fields.Float(string='Instruct Qty', digits=(16, 0), default=lambda self: self.product_uom_qty - self.instructed_qty)
    # 库存可用数
    qty_available = fields.Float(string='Available Qty', related='sale_order_line_id.product_id.s_actual_quantity',
                                 digits='Product Unit of Measure', default=0)
    display_available = fields.Float(string='Available Qty', compute="_compute_display_available")
    nre = fields.Float(string='NRE', compute="_compute_nre", digits=(16, 2), store=True, compute_sudo=True)
    unit_price = fields.Float(string='Unit price', related='sale_order_line_id.price_unit', store=True)
    state = fields.Selection(related='ship_instructions_id.state', store=True)
    incoterm = fields.Many2one('account.incoterms', string='Incoterm', related='ship_instructions_id.incoterm')
    sp_id = fields.Many2one('stock.picking', string='Delivery Note', related='ship_instructions_id.instructor_sp')
    ship_date = fields.Date(string='Instruction Date', related='ship_instructions_id.shipping_date', store=True)
    out_date = fields.Datetime(string='Out Date', related='ship_instructions_id.instructor_sp.date_done', store=True)
    actual_date = fields.Date(string='Actual Ship Date', related='ship_instructions_id.pd_id.actual_shipping_date', store=True)
    currency_field = fields.Many2one('res.currency', string="Currency", related='sale_order_id.currency_id', store=True)
    ttl_amount = fields.Monetary(string='Total Amount', compute_sudo=True, compute="_compute_ttl_amount", currency_field='currency_field', store=True)
    subtotal = fields.Float(string='Subtotal untax', compute='_compute_ttl_amount', store=True)
    payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', related='sale_order_id.payment_term_id')
    partner = fields.Many2one('res.partner', string='Partner', related='sale_order_id.partner_id', store=True)
    partner_code = fields.Char(string='Partner Code', related='partner.code')
    salesperson = fields.Many2one('res.users', string='Sales Person', related='sale_order_id.user_id')
    contact_address_inline = fields.Text(related='ship_instructions_id.delivery_address')
    package_ids = fields.One2many('sb.picking.list', 'shipping_instructions_line_id', string='Packages')
    mix = fields.Boolean(string='Mix Package', compute='_compute_mix')
    packed_qty = fields.Integer(string='Packed Qty', compute='_compute_packed_qty', store=True)
    sample_qty = fields.Integer('Sample Qty', compute='_compute_packed_qty', store=True)
    color = fields.Integer(string='Color', compute='_compute_color')
    date_code = fields.Char(related='package_ids.note')
    select = fields.Selection(related='ship_instructions_id.select')
    tt_n_weight = fields.Float(string='TTL N.W.(kg)', compute='_compute_package_detail')
    tt_g_weight = fields.Float(string='TTL G.W.(kg)', compute='_compute_package_detail')
    tt_pacakge = fields.Integer(string='Packages', compute='_compute_package_detail')
    sb_check_state = fields.Selection([
        ('draft', 'Not Checked'),
        ('checking', 'In Checking'),
        ('partial', 'Partially Checked'),
        ('done', 'Fully Checked'),
    ], string='Check Status', default='draft')
    invoice = fields.Many2one('account.move', string='Invoice', compute='_compute_invoice', compute_sudo=True, store=True)
    pd_id = fields.Many2one('sb.packing.delivery', string='Packing Delivery', related='ship_instructions_id.pd_id')
    mix_package_ids = fields.Many2many('sb.wrapped.in.box', string='Mix Package', compute='_compute_mix_package_ids')
    s_amount_total_usd = fields.Monetary(string="($)TTL Amount", currency_field='s_currency_id_usd',
                                         compute_sudo=True, store=True, compute='_compute_amount_total_usd')
    s_currency_id_usd = fields.Many2one('res.currency', default=lambda self: self.env.ref('base.USD'), readonly=True, store=True)
    other_cost = fields.Float(string='EXT', digits=(16, 2), compute="_compute_nre", store=True)
    other_cost_note = fields.Char(string='EXT Decs', compute="_compute_nre", store=True)
    irregular_qty = fields.Boolean(string='Irregular', default=False, help="If True, It will be allowed that instructed is not an integer multiple of PCS/SET")
    tax_id = fields.Many2many(comodel_name='account.tax', string='Tax', related='sale_order_line_id.tax_id')
    # 小计税额
    amount_tax = fields.Float(string='Amount Tax', compute='_compute_amount_tax', store=True)
    order_date = fields.Datetime(string='Order Date', related='sale_order_id.create_date', store=True)

    @api.depends('tax_id', 'delivery_qty')
    def _compute_amount_tax(self):
        for record in self:
            if record.tax_id:
                subtotal = record.nre + record.other_cost + (record.delivery_qty * record.unit_price)
                sub = subtotal * (1 + record.tax_id[0].amount / 100)
                subtotal = sub - subtotal
                record.amount_tax = subtotal or 0
            else:
                record.amount_tax = 0

    @api.depends('ttl_amount', 'currency_field', 'create_date', 'unit_price', 'delivered_qty')
    def _compute_amount_total_usd(self):
        usd = self.env.ref('base.USD')
        for order in self:
            if order.currency_field == usd:
                order.s_amount_total_usd = order.ttl_amount
            else:
                rate = order.currency_field._get_conversion_rate(
                    order.currency_field, usd, self.env.company.id, order.create_date)
                order.s_amount_total_usd = order.ttl_amount * rate

    @api.depends('package_ids')
    def _compute_mix_package_ids(self):
        for obj in self:
            obj.mix_package_ids = obj.package_ids.mapped('mix_box_id_ids')

    @api.depends('package_ids')
    def _compute_mix(self):
        for obj in self:
            obj.mix = any(package.mix_box_id_ids for package in obj.package_ids)

    @api.depends('ship_instructions_id.invoice_ids', 'sale_order_line_id')
    def _compute_invoice(self):
        for res in self:
            res.invoice = False
            invoice_lines = res.ship_instructions_id.invoice_ids.mapped('invoice_line_ids')
            # 遍历发票行，查找对应的 sale_order_line_id
            for line in invoice_lines:
                if line.sale_line_ids and res.sale_order_line_id in line.sale_line_ids:
                    res.invoice = line.move_id  # move_id 是 invoice_line 回到发票头的字段
                    break

    @api.depends('package_ids', 'mix_package_ids')
    def _compute_package_detail(self):
        for rec in self:
            carton = len(rec.mix_package_ids) + sum(
                rec.package_ids.filtered(lambda pl: pl not in rec.mix_package_ids.mapped('pl_ids')).mapped('package')
            )
            nw_ttl = sum(
                line.n_weight * line.package for line in rec.package_ids
                if line not in rec.mix_package_ids.mapped('pl_ids')
            ) + sum(
                line.n_weight for line in rec.mix_package_ids
            )
            gw_ttl = sum(
                line.g_weight * line.package for line in rec.package_ids
                if line not in rec.mix_package_ids.mapped('pl_ids')
            ) + sum(
                line.g_weight for line in rec.mix_package_ids
            )
            rec.tt_pacakge = carton
            rec.tt_n_weight = nw_ttl
            rec.tt_g_weight = gw_ttl

    def action_create_account_checking(self):
        # Gather active lines
        active_lines = self.env['ship.instructions.line'].browse(self._context.get('active_ids', []))
        customer = active_lines[0].partner.id
        for obj in active_lines:
            if obj.state != 'done':
                raise UserError(_('Please select the completed detail lines.'))
            if obj.partner.id != customer:
                raise UserError(_('Please select the same customer.'))
        # Only allow lines in 'draft' or 'partial' state
        invalid = active_lines.filtered(lambda l: l.sb_check_state not in ('draft', 'partial'))
        if invalid:
            names = ', '.join(invalid.mapped('name'))
            states = ', '.join(set(invalid.mapped('sb_check_state')))
            raise UserError(_(
                "Cannot start reconciliation for lines [%s] in state [%s]."
            ) % (names, states))
        # Mark lines as 'checking'
        active_lines.write({'sb_check_state': 'checking'})
        # Create account checking record
        checking = self.env['sb.account.checking'].sudo().create({
            'ship_instructions_lines': [(6, 0, active_lines.ids)]
        })

        unique_names = set()
        for line in active_lines:
            unique_names.add(line.name)
        invoice_records = self.env['account.move'].search([('name', 'in', list(unique_names))])
        for x in invoice_records:
            x.sb_checking_id = checking.id
        # Open the checking form
        return {
            'name': _('Account Checking'),
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'sb.account.checking',
            'res_id': checking.id,
            'type': 'ir.actions.act_window',
            'target': 'current',
        }

    # def action_create_account_checking(self):
    #     records = self.env['ship.instructions.line'].browse(
    #         self._context.get('active_ids', []))
    #     self.env['sb.account.checking'].sudo().create({
    #         'ship_instructions_lines': [(6, 0, records.ids)]
    #     })

    def _compute_color(self):
        for obj in self:
            if obj.sp_id and obj.sp_id.state == 'done':
                current_ship_instructions_id = obj.ship_instructions_id.id
                # 从 sale_order_line 获取所有相关的 stock.move
                stock_moves = obj.sale_order_line_id.move_ids
                # 筛选出对应的 stock.picking 中的 ship_instructions_id 等于当前的 ship_instructions_id
                filtered_stock_moves = stock_moves.filtered(
                    lambda move: move.picking_id.instruction_id.id == current_ship_instructions_id
                )  # 理论上 应该only one
                if filtered_stock_moves and filtered_stock_moves[0].picking_id.state == 'done':
                    if filtered_stock_moves[0].quantity == obj.delivery_qty:
                        obj.color = 3
                    else:
                        if filtered_stock_moves[0].quantity == 0:
                            obj.color = 1
                        else:
                            obj.color = 4
                else:
                    obj.color = 1
            else:
                obj.color = 1

    def compute_name(self):
        for line in self.env['ship.instructions.line'].search([]):
            parent = line.ship_instructions_id
            # 只处理状态为 'done' 的 ship.instructions
            if parent.state != 'done':
                continue
            # 如果 shipping_date 为空，则使用 create_date
            date_field = parent.shipping_date or parent.create_date
            if not date_field:
                continue
            # 如果日期字段为 datetime 类型，则转换成字符串格式 "YYYY-MM-DD"
            if isinstance(date_field, (datetime.datetime, datetime.date)):
                shipping_date_str = date_field.strftime('%Y-%m-%d')
            else:
                shipping_date_str = date_field
            try:
                date_obj = datetime.datetime.strptime(shipping_date_str, '%Y-%m-%d')
            except Exception:
                continue
            # 格式化为 "yymmdd" 格式，例如 "250330"
            date_str = date_obj.strftime('%y%m%d')
            # 查询当天所有状态为 'done' 的 ship.instructions，并按 id 排序
            done_lines = self.env['ship.instructions.line'].search([
                ('ship_instructions_id.state', '=', 'done'),
                ('ship_instructions_id.shipping_date', '=', shipping_date_str)
            ], order='ship_instructions_id')
            # 计算流水号（基于排序后的位置）
            seq = 1
            for idx, instr_line in enumerate(done_lines, start=1):
                if instr_line.ship_instructions_id.id == parent.id:
                    seq = idx
                    break
            # 格式化流水号为 3 位数字（如 "001"）
            seq_formatted = str(seq).zfill(3)
            new_code = "WTT" + date_str + seq_formatted
            # 将编码赋值给当前 line 的 name 字段
            line.name = new_code

    @api.depends('qty_available')
    def _compute_display_available(self):
        for obj in self:
            obj.display_available = obj.qty_available
            if obj.qty_available < 0:
                obj.display_available = 0

    @api.depends('package_ids', 'package_ids.qty', 'package_ids.package', 'package_ids.free_sample')
    def _compute_packed_qty(self):
        for obj in self:
            obj.packed_qty = sum(x.package * x.qty for x in obj.package_ids)
            obj.sample_qty = sum(x.free_sample for x in obj.package_ids if x.free_sample > 0)

    @api.depends('delivery_qty', 'unit_price', 'nre', 'other_cost', 'amount_tax')
    def _compute_ttl_amount(self):
        for obj in self:
            obj.subtotal = obj.delivery_qty * obj.unit_price + obj.nre + obj.other_cost
            obj.ttl_amount = obj.subtotal + obj.amount_tax

    @api.depends('sale_order_line_id')
    def _compute_nre(self):
        for x in self:
            for obj in x.sale_order_line_id.order_id.order_line:
                if obj.product_id.name == 'NRE':
                    if obj.qty_invoiced == 0:
                        x.nre = obj.price_unit
                    continue
                if obj.product_id.name == 'Additional Charge':
                    if obj.qty_invoiced == 0:
                        x.other_cost = obj.price_unit
                        x.other_cost_note = obj.name
                    continue

    @api.constrains("delivery_qty")
    def _check_delivery_qty(self):
        for obj in self:
            if obj.delivery_qty > (obj.product_uom_qty - obj.instructed_qty):
                raise ValidationError(_(f'Delivery Qty cannot be greater than remaining assignments Qty %s.') % (obj.product_uom_qty - obj.instructed_qty))
            if obj.delivery_qty < 0:
                raise ValidationError(_('Delivery Qty cannot be less than 0.'))

    @api.depends('product_id')
    @api.depends_context('show_name')
    def _compute_display_name(self):
        for res in self:
            if res._context.get('show_name'):
                res.display_name = res.name
            else:
                res.display_name = res.sale_order_id.name

    # @api.depends('sale_order_line_id.product_uom_qty', 'sale_order_line_id.qty_delivered')
    # def _compute_delivered_qty(self):
    #     for obj in self:
    #         obj.delivered_qty = obj.sale_order_line_id.product_uom_qty - obj.sale_order_line_id.qty_delivered

    @api.onchange('sale_order_id', 'sale_order_line_id')
    def onchange_order_line(self):
        if not self.sale_order_id:
            return

        for sl in self.sale_order_id.order_line:
            if sl.product_template_id.detailed_type == 'product':
                self.sale_order_line_id = sl.id
                self.sale_order_line_id.ship_instructions_id = self.ship_instructions_id.id
        # self.delivered_qty = self.sale_order_line_id.product_uom_qty - self.sale_order_line_id.qty_delivered


class ShippingInstructions(models.Model):
    _name = 'ship.instructions'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _description = "Ship Instructions"
    _order = 'create_date desc, id desc'

    name = fields.Char()
    instructor = fields.Many2one('res.users', string='Instructor')
    sol_ids = fields.One2many('sale.order.line', 'ship_instructions_id', string='Sales Orders', store=True)
    sale_ids = fields.Many2many('sale.order', string='Sales', compute='_compute_stock_picking', store=True)
    actual_date = fields.Date(string='Actual Ship Date', related='pd_id.actual_shipping_date')
    state = fields.Selection([
        ('cancel', 'Cancel'),
        ('readying', 'Readying'),
        ('command', 'Command'),
        ('pd', 'Package'),
        ('shipping_confirmation', 'Waiting Confirm'),
        ('logistics', 'Logistics'),
        ('wait_delivery', 'Waiting to ship'),
        ('done', 'Done'),
    ], default='readying', string='State', tracking=True, store=True, copy=False)
    stock_picking_ids = fields.Many2many('stock.picking', string='Stock Pickings', compute='_compute_stock_picking', store=True)
    finish_date = fields.Date(string='Finish Date')
    line_ids = fields.One2many('ship.instructions.line', 'ship_instructions_id')
    unique_line_ids = fields.Many2many('ship.instructions.line', compute='_compute_unique_line_ids', store=False)
    partner_id = fields.Many2one('res.partner', 'Customer')
    delivery_address = fields.Text(string='Delivery Address', compute='_compute_address', store=True)
    note = fields.Text(string='Note')
    shipping_id = fields.Many2one('res.partner', string='Shipping Info')
    incoterm = fields.Many2one('account.incoterms', string='Incoterm', store=True, related='shipping_id.incoterm')
    invoice_ids = fields.Many2many('account.move', string='Invoices')
    invoice_boolean = fields.Boolean(string='Invoice', compute='_invoice_boolean')
    contact_address_inline = fields.Char(string='Shipping Address', related='shipping_id.contact_address_complete', store=True)
    indicate_date = fields.Date(string='Indicate Date')
    shipping_date = fields.Date(string='Shipping Date')
    contacts = fields.Char(string='Contacts', related='shipping_id.contact_name')
    phone = fields.Char(string='Phone', related='shipping_id.phone')
    instructor_sp = fields.Many2one("stock.picking", string='instruction sp')
    currency_id = fields.Many2one('res.currency', string='Currency', related='partner_id.currency_id', store=True)
    payment_term = fields.Many2one("account.payment.term", string='Payment Term', related='partner_id.property_payment_term_id')
    unnre_ttl = fields.Monetary(string='FG TTL', compute='_compute_ttl', store=True, currency_field='currency_id')
    nre_ttl = fields.Monetary(string='NRE TTL', compute='_compute_ttl', store=True, currency_field='currency_id')
    other_cost_ttl = fields.Monetary(string='EXT TTL', compute='_compute_ttl', store=True, currency_field='currency_id')
    amount_ttl = fields.Monetary(string='Amount TTL', compute='_compute_ttl', store=True, currency_field='currency_id')
    tax_ttl = fields.Monetary(string='TAX TTL', compute='_compute_ttl', store=True, currency_field='currency_id')
    subtotal_ttl = fields.Monetary(string='Subtotal untax TTL', compute='_compute_ttl', store=True, currency_field='currency_id')
    pd_id = fields.Many2one('sb.packing.delivery', string='Packing Delivery')
    pick_lists = fields.One2many('sb.picking.list', related='pd_id.pick_lists', string='Pick Lists')
    shipping_method = fields.Many2one('s.shipping.method', string='Shipping Method')
    bank_num = fields.Char(string="Account Num.")
    carton_number = fields.Integer(string='Cartons(Box)', related='pd_id.carton_number')
    nw_ttl = fields.Float(string='N.W. TTL(Kg)', related='pd_id.nw_ttl')
    gw_ttl = fields.Float(string='G.W. TTL(Kg)', related='pd_id.gw_ttl')
    forwarder = fields.Char(string='Forwarder', related='pd_id.forwarder')
    freight_charges = fields.Float(string='Freight Charges', related='pd_id.freight_charges')
    declaration_form_no = fields.Char(string='Export declaration number', related='pd_id.declaration_form_no')
    tr_no = fields.Char(string='TR No.', related='pd_id.tr_no')
    ttl_instructions = fields.Integer(string='TTL Instructions', compute='_compute_ttl_instructions', store=True)
    bear_freight = fields.Boolean(string='Customer Bear Freight')
    mix_package_ids = fields.One2many('sb.wrapped.in.box', string='Mix Package', related='pd_id.mix_package_ids')
    mix_package = fields.Boolean(string='Mix Package', related='pd_id.mix_package')
    select = fields.Selection(string='Billing Logic', selection=[
        ('standard', 'Standard'),
        ('po', 'Created For Each PO'),
        ('nre', 'Independent NRE'),
    ], default='standard')
    warehouse_user_emails = fields.Char(compute='_compute_warehouse_user_emails', store=False)
    warehouse_user_ids = fields.Many2many('res.users', compute='_compute_warehouse_user_ids', store=False)
    free_cost = fields.Boolean('Free Cost', default=False)
    inv_pl_name = fields.Char(string='Invoice Packing List Name', compute='_compute_inv_pl_name')
    irregular_qty = fields.Boolean(string='Irregular', default=False, help="If True, It will be allowed that instructed is not an integer multiple of PCS/SET")
    document_ids = fields.One2many(related='pd_id.document_ids')

    def _compute_inv_pl_name(self):
        for rec in self:
            if rec.invoice_ids:
                rec.inv_pl_name = f"{rec.invoice_ids[0].name}"
            else:
                rec.inv_pl_name = ""

    def write(self, vals):
        res = super(ShippingInstructions, self).write(vals)

        # 只有在 irregular_qty 被設成 0/False 時才清空 line
        if vals.get('irregular_qty') is False:
            for rec in self:
                rec.line_ids.write({'irregular_qty': False})

        return res

    def _compute_warehouse_user_emails(self):
        for rec in self:
            warehouse_dept = self.env['hr.department'].search([('code', '=', 'WHS')], limit=1)
            employees = self.env['hr.employee'].search([('department_id', '=', warehouse_dept.id)])
            emails = employees.mapped('user_id.partner_id.email')
            rec.warehouse_user_emails = ','.join(filter(None, emails))

    def _compute_warehouse_user_ids(self):
        for rec in self:
            warehouse_dept = self.env['hr.department'].search([('code', '=', 'WHS')], limit=1)
            employees = self.env['hr.employee'].search([('department_id', '=', warehouse_dept.id)])
            rec.warehouse_user_ids = employees.mapped('user_id').filtered(lambda u: u.active)

    def button_test_email(self):
        template = self.env.ref('sb_sale.email_template_confirm_instruction')
        mail = template.sudo().send_mail(self.id, force_send=True)
        channel = self.env['discuss.channel'].sudo().search([('uuid', '=', 'ySHCgPsWJM')], limit=1)
        action = self.env.ref('sb_sale.sb_pd_pack_action')
        menu = self.env.ref('sb_sale.menu_sb_pd_pack')
        record_id = self.pd_id.id

        url = f"/web?debug=1#id={record_id}&action={action.id}&model=sb.packing.delivery&view_type=form&menu_id={menu.id}"
        message_body = Markup(
            _("📦 New delivery order <b>%(name)s</b> is confirmed. Please process it promptly.") % {'name': self.name}
            + "<br/>"
            + _("👉 <a href='%(url)s' target='_blank'>Click here to view</a>") % {'url': url}
        )
        channel.sudo().message_post(
            body=message_body,
            subject=_("New delivery order"),
            subtype_xmlid='mail.mt_comment',
            message_type="comment",
            author_id=2
        )
        # for user in self.warehouse_user_ids:
        #     self.activity_schedule(
        #         activity_type_id=self.env.ref('mail.mail_activity_data_todo').id,
        #         summary='Urgent: PD Generated',
        #         note=f'Please follow up. <a href="/web#id={self.pd_id.id}&model=sb.packing.delivery&view_type=form" target="_blank">Click to open</a>',
        #         date_deadline=self.shipping_date,
        #         user_id=user.id,
        #         icon='fa-cubes'
        #     )

    @api.constrains('shipping_date')
    def _check_shipping_date(self):
        for record in self:
            if record.shipping_date:
                today = fields.Date.context_today(self)
                if record.shipping_date <= today:
                    raise ValidationError(_("Shipping Date cannot be today or a past date."))

    def button_cancel(self):
        for rec in self:
            if rec.pd_id:
                rec.pd_id.state = 'cancel'
            rec.state = 'cancel'
            for x in rec.line_ids:
                # 重算
                x.sale_order_id._compute_indicate_state()

    def unlink(self):
        self.sudo()
        for res in self:
            if res.state != 'readying' or res.pd_id:
                raise UserError(_('Only records in "Readying" state without a linked Packing Delivery can be deleted.'))
            res.button_cancel()
        super(ShippingInstructions, self).unlink()
        return self.env.ref('sb_sale.sb_sale_tree_view_action').read()[0]

    @api.depends('line_ids')
    def _compute_unique_line_ids(self):
        for rec in self:
            unique_names = set()
            unique_lines = self.env['ship.instructions.line']
            for line in rec.line_ids:
                if line.name not in unique_names:
                    if rec.state not in ['cancel', 'pd', 'readying', 'command']:
                        if line.name and line.name.startswith('PL'):
                            continue
                        unique_names.add(line.name)
                    else:
                        unique_names.add(line.name)
                    unique_lines |= line
            rec.unique_line_ids = unique_lines

    @api.depends('line_ids', 'line_ids.delivery_qty')
    def _compute_ttl_instructions(self):
        for obj in self:
            obj.ttl_instructions = sum(line.delivery_qty for line in obj.line_ids)

    @api.depends('line_ids', 'line_ids.ttl_amount', 'line_ids.nre', 'line_ids.other_cost')
    def _compute_ttl(self):
        for obj in self:
            unnre_ttl = 0
            nre_ttl = 0
            amount_ttl = 0
            other_cost_ttl = 0
            tax_ttl = 0
            subtotal_ttl = 0

            for line in obj.line_ids:
                unnre_ttl += line.unit_price * line.delivery_qty
                nre_ttl += line.nre
                amount_ttl += line.ttl_amount
                other_cost_ttl += line.other_cost
                tax_ttl += line.amount_tax
                subtotal_ttl += line.subtotal

            obj.unnre_ttl = unnre_ttl
            obj.nre_ttl = nre_ttl
            obj.amount_ttl = amount_ttl
            obj.tax_ttl = tax_ttl
            obj.other_cost_ttl = other_cost_ttl
            obj.subtotal_ttl = subtotal_ttl

    @api.depends('invoice_ids')
    def _invoice_boolean(self):
        for obj in self:
            # 判断是否有发票并且发票状态不是 'cancel'（已取消）
            if obj.invoice_ids and all(invoice.state != 'cancel' for invoice in obj.invoice_ids):
                obj.invoice_boolean = True
            else:
                obj.invoice_boolean = False

    @api.model
    def create_invoice(self):
        # Search for the customer invoice journal
        customer_invoice_journal = self.env['account.journal'].sudo().search([('code', '=', 'INV')], limit=1)
        if not customer_invoice_journal:
            raise UserError(_("Configuration Error: No journal named 'Customer Invoice' found. Please check the configuration."))

        # Search for the sales account
        sales_account = self.env['account.account'].sudo().search([('code', '=', '410000')], limit=1)
        if not sales_account:
            raise UserError(_("Configuration Error: No account with code '410000' found. Please check the configuration."))

        # Determine which sequence to use based on the currency
        sequence_code_to_use = self.env.ref('sb_sale.seq_sb_pack_delivery')  # Default sequence
        if self.currency_id.id == self.env.ref('base.THB').id:
            sequence_code_to_use = self.env.ref('sb_sale.seq_thai_inv_delivery')  # Thai sequence

        def _prepare_invoice_line_data(invoice_id, source_so_line_record, quantity, price_unit):
            """
            准备用于创建 account.move.line 的数据字典。
            同时返回原始的 sale.order.line 记录，以便后续批量更新其 'invoice_lines' 字段。

            :param invoice_id: 关联的发票ID (account.move.id)
            :param source_so_line_record: 对应的 sale.order.line 记录
            :param quantity: 发票行数量
            :param price_unit: 发票行单价
            :return: 包含 'vals' (发票行创建值) 和 'original_so_line_record' 的字典
            """
            if not source_so_line_record:
                raise UserError(_("Internal Error: Attempted to create an invoice line when the sales order line record was empty."))

            return {
                'vals': {
                    'move_id': invoice_id,
                    'product_id': source_so_line_record.product_id.id,
                    'quantity': quantity,
                    'price_unit': price_unit,
                    'account_id': sales_account.id,  # 使用预查找的销售账户
                    'product_uom_id': source_so_line_record.product_uom.id,  # 使用销售订单行的计量单位
                    'tax_ids': source_so_line_record.tax_id.ids,
                },
                'original_so_line_record': source_so_line_record
            }

        for rec in self:
            base_invoice_vals = {
                'partner_id': rec.partner_id.id,
                'invoice_payment_term_id': rec.payment_term.id,
                'invoice_date': fields.Date.today() if not rec.actual_date else rec.actual_date,
                'currency_id': rec.currency_id.id,
                'move_type': 'out_invoice',
                'journal_id': customer_invoice_journal.id,  # 使用预查找的日记账
                'partner_shipping_id': rec.shipping_id.id,
                'invoice_incoterm_id': rec.incoterm.id if rec.incoterm else False,
                'invoice_user_id': rec.instructor.id,
            }
            if self.select == 'standard':
                # 标准模式：所有产品行和所有相关服务行都合并到一张发票中
                invoice_vals = base_invoice_vals.copy()
                # 获取发票的序列号
                seq = sequence_code_to_use._next()
                if not seq:
                    raise UserError(_("The sequence '%s' is not configured or cannot be generated.") % sequence_code_to_use)

                invoice_vals.update({
                    'name': seq,  # 使用生成的序列号作为发票名称
                    's_instruction_num': self.name,
                })

                invoice = self.env['account.move'].sudo().create(invoice_vals)

                all_invoice_line_data_to_create = []  # 收集所有发票行的数据（包含vals和原始so_line_record）

                # 1. 处理来自当前记录的产品行 (rec.line_ids)
                for line in rec.line_ids:
                    # 确保有销售订单行且有实际打包数量
                    if not line.sale_order_line_id or line.packed_qty == 0:
                        continue
                    price_unit = line.sale_order_line_id.price_unit if not self.free_cost else 0.01
                    line.name = seq
                    all_invoice_line_data_to_create.append(_prepare_invoice_line_data(
                        invoice.id, line.sale_order_line_id, line.packed_qty, price_unit
                    ))

                # 2. 查找并处理所有相关销售订单中的未开票服务行
                related_sale_orders = rec.line_ids.mapped('sale_order_line_id.order_id')
                # 查找这些销售订单中所有未完全开票的服务类型行
                unbilled_service_lines = self.env['sale.order.line'].sudo().search([
                    ('order_id', 'in', related_sale_orders.ids),
                    ('product_id.detailed_type', '=', 'service'),
                    ('qty_invoiced', '<', 1),
                ])

                for so_line in unbilled_service_lines:
                    qty_to_invoice = so_line.product_uom_qty - so_line.qty_invoiced
                    if qty_to_invoice > 0:
                        all_invoice_line_data_to_create.append(_prepare_invoice_line_data(
                            invoice.id, so_line, qty_to_invoice, so_line.price_unit
                        ))

                # 批量创建发票行并关联回原始销售订单行
                if all_invoice_line_data_to_create:
                    # 提取所有发票行创建所需的值字典
                    invoice_line_vals_list = [d['vals'] for d in all_invoice_line_data_to_create]
                    # 批量创建发票行
                    created_move_lines = self.env['account.move.line'].sudo().create(invoice_line_vals_list)

                    # 遍历新创建的发票行和它们对应的原始数据，进行关联
                    for created_ml, original_data in zip(created_move_lines, all_invoice_line_data_to_create):
                        original_so_line_record = original_data['original_so_line_record']
                        if original_so_line_record:
                            # 使用 (4, id) 将新创建的发票行关联到销售订单行
                            original_so_line_record.write({'invoice_lines': [(4, created_ml.id)]})

                rec.write({'invoice_ids': [(4, invoice.id)]})  # 将发票关联到当前记录
                invoice.sb_button_confirm()

            elif self.select == 'po':
                # PO 模式：根据 PO 字段对行进行分组，每个 PO 组创建一张发票
                po_grouped_lines = defaultdict(list)  # 使用 defaultdict 简化分组
                for line in rec.line_ids:
                    if line.packed_qty == 0:
                        continue
                    po_number = line.po_name
                    if po_number:
                        po_grouped_lines[po_number].append(line)
                    else:
                        self.env.cr.warning(_("Warning: Line '%s' has no associated PO number and will be skipped in PO grouped invoice creation.") % line.name)

                for po_number, lines_in_group in po_grouped_lines.items():
                    # 为当前 PO 组创建的发票获取序列号
                    seq = sequence_code_to_use._next()
                    if not seq:
                        raise UserError(_("The sequence '%s' is not configured or cannot be generated.") % sequence_code_to_use)

                    # 从该 PO 组的第一行获取其关联的销售订单，以确定发票的伙伴和货币
                    first_line_in_group = lines_in_group[0]
                    so_for_invoice = first_line_in_group.sale_order_id
                    if not so_for_invoice:
                        raise UserError(_("Error: Lines in PO group '%s' have no associated sales order. Cannot create invoice.") % po_number)

                    invoice_vals = base_invoice_vals.copy()
                    invoice_vals.update({
                        'name': seq,  # 使用生成的序列号作为发票名称
                        's_instruction_num': self.name,  # 使用 PO 号作为指令号
                        'partner_id': so_for_invoice.partner_id.id,  # 从销售订单获取伙伴
                        'currency_id': so_for_invoice.currency_id.id,  # 从销售订单获取货币
                    })
                    invoice = self.env['account.move'].sudo().create(invoice_vals)

                    all_invoice_line_data_to_create = []  # 收集此发票的所有行数据

                    # 1. 添加当前 PO 组内的所有产品行到此发票
                    for line in lines_in_group:
                        if line.packed_qty == 0:
                            continue
                        line.name = seq
                        all_invoice_line_data_to_create.append(_prepare_invoice_line_data(
                            invoice.id, line.sale_order_line_id, line.packed_qty, line.sale_order_line_id.price_unit
                        ))

                    # 2. 查找并添加与此 PO 组相关联的所有销售订单中的未开票服务行
                    related_sale_orders_for_services = self.env['sale.order']
                    for line in lines_in_group:  # 收集此 PO 组中所有行关联的销售订单
                        if line.sale_order_id:
                            related_sale_orders_for_services |= line.sale_order_id

                    unbilled_service_lines = self.env['sale.order.line'].sudo().search([
                        ('order_id', 'in', related_sale_orders_for_services.ids),
                        ('product_id.detailed_type', '=', 'service'),
                        ('qty_invoiced', '<', 1),  # 未完全开票的服务行
                    ])

                    for so_line in unbilled_service_lines:
                        qty_to_invoice = so_line.product_uom_qty - so_line.qty_invoiced
                        if qty_to_invoice > 0:
                            all_invoice_line_data_to_create.append(_prepare_invoice_line_data(
                                invoice.id, so_line, qty_to_invoice, so_line.price_unit
                            ))

                    # 批量创建发票行并关联回原始销售订单行
                    if all_invoice_line_data_to_create:
                        invoice_line_vals_list = [d['vals'] for d in all_invoice_line_data_to_create]
                        created_move_lines = self.env['account.move.line'].sudo().create(invoice_line_vals_list)

                        for created_ml, original_data in zip(created_move_lines, all_invoice_line_data_to_create):
                            original_so_line_record = original_data['original_so_line_record']
                            if original_so_line_record:
                                original_so_line_record.write({'invoice_lines': [(4, created_ml.id)]})

                    rec.write({'invoice_ids': [(4, invoice.id)]})  # 将发票关联到当前主记录
                    invoice.sb_button_confirm()  # 验证并过账发票

            elif self.select == 'nre':
                # NRE 模式：产品行和服务行分别创建发票

                # 1. 过滤并准备产品行
                product_lines_to_process = rec.line_ids.filtered(
                    lambda l: l.sale_order_line_id and l.packed_qty > 0 and l.product_id.detailed_type != 'service'
                )
                seq = sequence_code_to_use._next()
                # 创建产品发票
                product_invoice = None
                if product_lines_to_process:
                    product_invoice_vals = base_invoice_vals.copy()
                    # 产品发票的名称和指令号可以使用第一行的名称
                    product_invoice_vals.update({
                        'name': seq,
                        's_instruction_num': seq,
                    })
                    product_invoice = self.env['account.move'].sudo().create(product_invoice_vals)

                    product_invoice_line_data_to_create = []
                    for line in product_lines_to_process:
                        line.name = seq
                        product_invoice_line_data_to_create.append(_prepare_invoice_line_data(
                            product_invoice.id, line.sale_order_line_id, line.packed_qty, line.sale_order_line_id.price_unit
                        ))

                    if product_invoice_line_data_to_create:
                        invoice_line_vals_list = [d['vals'] for d in product_invoice_line_data_to_create]
                        created_move_lines = self.env['account.move.line'].sudo().create(invoice_line_vals_list)

                        for created_ml, original_data in zip(created_move_lines, product_invoice_line_data_to_create):
                            original_so_line_record = original_data['original_so_line_record']
                            if original_so_line_record:
                                original_so_line_record.write({'invoice_lines': [(4, created_ml.id)]})

                    rec.write({'invoice_ids': [(4, product_invoice.id)]})
                    product_invoice.sb_button_confirm()

                # 2. 创建服务发票
                # 找出所有相关联的 SO (从 rec.line_ids 反推)
                relevant_sale_orders = rec.line_ids.mapped('sale_order_line_id.order_id')
                all_unbilled_service_lines = self.env['sale.order.line'].sudo().search([
                    ('order_id', 'in', relevant_sale_orders.ids),
                    ('product_id.detailed_type', '=', 'service'),
                    ('qty_invoiced', '<', 1),  # 确保只选择未完全开票的服务行
                ])

                if all_unbilled_service_lines:
                    service_invoice_vals = base_invoice_vals.copy()
                    seq = sequence_code_to_use._next()
                    service_invoice_vals.update({
                        'name': seq,
                        's_instruction_num': self.name,
                        'ref': product_invoice.name if product_invoice else self.name,
                    })
                    service_invoice = self.env['account.move'].sudo().create(service_invoice_vals)

                    service_invoice_line_data_to_create = []
                    for so_line in all_unbilled_service_lines:
                        qty_to_invoice = so_line.product_uom_qty - so_line.qty_invoiced
                        if qty_to_invoice > 0:  # 确保只开未开票的数量
                            service_invoice_line_data_to_create.append(_prepare_invoice_line_data(
                                service_invoice.id, so_line, qty_to_invoice, so_line.price_unit
                            ))

                    if service_invoice_line_data_to_create:
                        invoice_line_vals_list = [d['vals'] for d in service_invoice_line_data_to_create]
                        created_move_lines = self.env['account.move.line'].sudo().create(invoice_line_vals_list)

                        for created_ml, original_data in zip(created_move_lines, service_invoice_line_data_to_create):
                            original_so_line_record = original_data['original_so_line_record']
                            if original_so_line_record:
                                original_so_line_record.write({'invoice_lines': [(4, created_ml.id)]})

                    rec.write({'invoice_ids': [(4, service_invoice.id)]})
                    service_invoice.sb_button_confirm()

    @api.depends('shipping_id')
    def _compute_address(self):
        for obj in self:
            country = obj.shipping_id.country_id.name + ' ' if obj.shipping_id.country_id else ''
            state = obj.shipping_id.state_id.name + ' ' if obj.shipping_id.state_id else ''
            city = obj.shipping_id.city + ' ' if obj.shipping_id.city else ''
            financial_street = obj.shipping_id.street + ' ' if obj.shipping_id.street else ''
            financial_street2 = obj.shipping_id.street2 + ' ' if obj.shipping_id.street2 else ''
            obj.delivery_address = country + state + city + financial_street + '\n' + financial_street2

    @api.onchange('line_ids')
    def add_button(self):
        return {
            'name': 'Shipping Instruction',
            'type': 'ir.actions.act_window',
            'res_model': 'ship.instructions.wizard',
            'view_mode': 'form',
            'target': 'new',
            'context': {
                'default_partner_id': self.partner_id.id,
                'default_ship_instructions_id': self.id,
                'default_sale_ids': [(4, so.id) for so in self.sale_ids if so]
            }
        }

    @api.depends('line_ids', 'sol_ids.move_ids')
    def _compute_stock_picking(self):
        stock_picking_ids = []
        sale_ids = []
        for line in self.line_ids:
            # 记录销售订单ID
            if line.sale_order_id:
                sale_ids.append(line.sale_order_id.id)
            # 从销售订单行中获取关联的 stock.move，然后获取对应的出货单(picking)
            for move in line.sale_order_line_id.move_ids:
                if move.picking_id and move.picking_id.state != 'cancel':
                    stock_picking_ids.append(move.picking_id.id)
        # 去重
        stock_picking_ids = list(set(stock_picking_ids))
        sale_ids = list(set(sale_ids))
        self.update({
            "stock_picking_ids": [(6, 0, stock_picking_ids)],
            "sale_ids": [(6, 0, sale_ids)],
        })

    def button_command(self):
        for obj in self:
            if obj.pd_id:
                raise ValidationError(_('The packing delivery has been generated, Perhaps there is something abnormal with your network.💦'))
            for line in obj.line_ids:
                if line.delivery_qty <= 0:
                    raise ValidationError(_('The instructions quantity must be greater than 0.'))
                elif line.delivery_qty > line.product_uom_qty - line.instructed_qty:
                    raise ValidationError(
                        _(f'The number of instructions must be less than the number of assignments remaining.\n %s Remaining assignments: %s') % (
                            line.po_name, line.product_uom_qty - line.instructed_qty))
                elif not (obj.irregular_qty and line.irregular_qty):
                    pcs_in_set = line.product_id.product_tmpl_id.uom_value_ids.filtered(lambda x: x.uom_id.name == "SET").uom_value
                    if pcs_in_set and line.delivery_qty % pcs_in_set != 0:
                        raise ValidationError(
                            _(f'The PO %s assignment Qty must be an integer multiple of PCS/SET.\n'
                              f'The current PCS/SET ratio is %s.') % (line.po_name, pcs_in_set)
                        )
            obj.indicate_date = fields.Date.today()

        def create_pick_list(obj, pd_id, line, seq):
            line.name = seq
            self.env['sb.picking.list'].sudo().create({
                'pd_id': pd_id,
                'shipping_instructions_id': obj.id,
                'shipping_instructions_line_id': line.id,
            })

        for obj in self:
            obj.state = 'pd'
            pd = self.env['sb.packing.delivery'].sudo().create({
                'ship_instructions_id': obj.id,
                'name': obj.name,
            })
            obj.pd_id = pd.id
            index = 1
            if obj.select != 'po':
                # seq = self.env['ir.sequence'].next_by_code('sb.packing.delivery')
                for line in obj.line_ids:
                    create_pick_list(obj, pd.id, line, f"PL-{index}")
            else:
                po_grouped_lines = {}
                for line in obj.line_ids:
                    po_number = line.po_name
                    if po_number not in po_grouped_lines:
                        po_grouped_lines[po_number] = []
                    po_grouped_lines[po_number].append(line)

                for po_number, lines_in_group in po_grouped_lines.items():
                    seq = f"PL-{index}"
                    index += 1
                    for line in lines_in_group:
                        create_pick_list(obj, pd.id, line, seq)

            # === 邮件发送 ===
            template = self.env.ref('sb_sale.email_template_confirm_instruction')
            try:
                template.send_mail(obj.id, force_send=True)
            except Exception as e:
                email_error = True  # 邮件失败不报错，流程继续
                # self.env.user.notify_warning(message="⚠ 邮件发送失败：请检查邮件配置。", title="邮件提醒失败", sticky=True)

            # === 频道通知 ===
            channel = self.env['discuss.channel'].sudo().search([('uuid', '=', 'ySHCgPsWJM')], limit=1)
            action = self.env.ref('sb_sale.sb_pd_pack_action')
            menu = self.env.ref('sb_sale.menu_sb_pd_pack')
            record_id = obj.pd_id.id

            url = f"/web?debug=1#id={record_id}&action={action.id}&model=sb.packing.delivery&view_type=form&menu_id={menu.id}"
            message_body = Markup(
                _("📦 New delivery order <b>%(name)s</b> is confirmed. Please process it promptly.") % {'name': obj.name}
                + "<br/>"
                + _("👉 <a href='%(url)s' target='_blank'>Click here to view</a>") % {'url': url}
            )
            channel.sudo().message_post(
                body=message_body,
                subject=_("New delivery order"),
                subtype_xmlid='mail.mt_comment',
                message_type="comment",
                author_id=self.env.user.partner_id.id  # 自动使用当前用户
            )
            # message = self._get_rainbowman_message()
            # if message:
            #     return {
            #         'effect': {
            #             'fadeout': 'slow',
            #             'message': message,
            #             'img_url': '/web/image/%s/%s/image_1024' % (
            #                 self.team_id.user_id._name, self.team_id.user_id.id) if self.team_id.user_id.image_1024 else '/web/static/img/smile.svg',
            #             'type': 'rainbow_man',
            #         }
            #     }
            # return True

    def button_return(self):
        for obj in self:
            obj.state = 'pd'
            obj.pd_id.state = 'pack'

    def _get_picking_vals(self, sample=False):
        if sample:
            return {
                'partner_id': self.shipping_id.id,
                'origin': self.name + ' - Free Samples',
                'name': 'FS-' + self.name,  # 調撥名稱，區分樣品
                'picking_type_id': self.env.ref('stock.picking_type_out').id,
                'location_id': self.env.ref('stock.stock_location_stock').id,
                'location_dest_id': self.partner_id.property_stock_customer.id,
                'scheduled_date': fields.Datetime.now(),
                'instruction_id': self.id,
                # 關鍵：唔關聯銷售訂單，避免影響銷售訂單數量
            }
        else:
            return {
                'partner_id': self.shipping_id.id,  # 客戶
                # 來源（關聯嘅銷售訂單名稱）
                'origin': ', '.join([soline.name for soline in self.sale_ids]) if self.sale_ids else '',
                'name': self.name,  # 調撥名稱，區分包裝數量
                'picking_type_id': self.env.ref('stock.picking_type_out').id,  # 出庫類型
                'location_id': self.env.ref('stock.stock_location_stock').id,  # 源位置：庫存
                'location_dest_id': self.partner_id.property_stock_customer.id,  # 目標位置：客戶庫存位置
                'scheduled_date': fields.Datetime.now(),  # 計劃日期
                'instruction_id': self.id  # 關聯到當前指令記錄
            }

    def confirm_shipment(self, admin=False):
        """
        'admin=True' 表示這個操作是由包裝部門發起的。
        此方法已更新，用於處理包裝數量和免費樣品的分開調撥。
        """
        try:
            # 遍歷所有關聯嘅銷售訂單，檢查是否有訂單已全部發貨
            for so in self.sale_ids:
                if so.delivery_status == 'full':
                    raise ValidationError(_('This order {} has all been sent out').format(so.name))

            self._check_repeat()  # 確保明細行沒有重複

            total_packed_qty = 0  # 總包裝數量
            total_free_sample_qty = 0  # 總免費樣品數量

            # 遍歷明細行進行初步驗證同數量匯總
            for line in self.line_ids:
                if line.delivery_qty <= 0:
                    raise ValidationError(_('The indicated quantity must be greater than 0.'))
                elif line.delivery_qty > line.product_uom_qty - line.instructed_qty:
                    raise ValidationError(
                        _(f'The number of instructions must be less than the number of assignments remaining.\n %s Remaining assignments:%s') % (
                            line.po_name, line.product_uom_qty - line.instructed_qty))

                # 如果唔係管理員（包裝部門）操作，且指示數量與包裝數量不符，則強制一致
                if line.delivery_qty != line.packed_qty and not admin:
                    line.delivery_qty = line.packed_qty

                total_packed_qty += line.packed_qty
                total_free_sample_qty += line.sample_qty

            if total_packed_qty == 0 and total_free_sample_qty == 0:
                raise ValidationError(_('The quantity of packaging and free samples is 0. Please check.'))

            fg_loc = self.env['stock.location'].search([('barcode', '=', 'WH-STOCK-FINISHED-PRODUCT')], limit=1)
            if not fg_loc:
                raise ValidationError(_('Stock location "Finished Goods" not found.'))

            out_dict = {}  # 用於追踪每個庫存量（quant）已用於包裝數量嘅部分

            # --- 1. 為包裝數量創建出庫調撥 (Picking) ---
            packed_picking = self.env['stock.picking'].sudo()
            if total_packed_qty > 0:

                # 創建包裝數量出庫調撥記錄
                packed_picking |= packed_picking.create(self._get_picking_vals())

                # 遍歷明細行，為包裝數量創建庫存移動
                for line in self.line_ids:
                    if line.packed_qty == 0:
                        continue  # 如果冇包裝數量，則跳過

                    total_move_qty_packed = line.packed_qty  # 本次移動嘅總包裝數量
                    if total_move_qty_packed <= 0:
                        continue

                    # 遍歷明細行下嘅所有包裝及其日期批次，以查找合適嘅庫存量
                    picking_list_line_ids = line.package_ids.mapped('picking_list_lines').sorted(lambda x: x.product_id.id)
                    for product_id, g in groupby(picking_list_line_ids, key=lambda x: x.product_id):
                        if float_is_zero(total_move_qty_packed, precision_rounding=product_id.uom_id.rounding):
                            break
                        g_line_ids = self.env['sb.picking.list.line'].concat(*g)
                        move_total = 0
                        move_line_vals_list = []
                        for g_line in g_line_ids:
                            move_line_qty = min([total_move_qty_packed, g_line.quantity])
                            if float_is_zero(move_line_qty, precision_rounding=product_id.uom_id.rounding):
                                break
                            move_line_vals_list.append((0, 0, {
                                # 'quant_id': g_line.id,  # 關聯嘅庫存量
                                'location_id': g_line.quant_id.location_id.id,  # 源位置
                                'location_dest_id': packed_picking.location_dest_id.id,  # 目標位置
                                'product_uom_id': product_id.uom_id.id,  # 產品計量單位
                                'quantity': move_line_qty,  # 完成數量
                                'lot_id': g_line.quant_id.lot_id.id,  # 批次號
                                'product_id': product_id.id,
                            }))

                            # 记录出货数据
                            if g_line in out_dict:
                                out_dict[g_line] += move_line_qty
                            else:
                                out_dict[g_line] = move_line_qty

                            move_total += move_line_qty
                            total_move_qty_packed -= move_line_qty

                        if move_line_vals_list:
                            self.env['stock.move'].sudo().create({
                                'name': product_id.display_name,  # 移動名稱，區分包裝
                                'product_id': product_id.id,
                                'product_uom_qty': move_total,
                                'sale_line_id': line.sale_order_line_id.id,  # 關聯到銷售訂單行
                                'product_uom': product_id.uom_id.id,
                                'location_id': packed_picking.location_id.id,
                                'location_dest_id': packed_picking.location_dest_id.id,
                                'picking_id': packed_picking.id,
                                'move_line_ids': move_line_vals_list,  # 關聯庫存移動明細行
                                'origin': self.name,
                            })

                # 確認並嘗試驗證包裝數量嘅出庫調撥
                packed_picking.sudo().action_confirm()
                packed_success = False  # 初始化變量
                try:
                    packed_picking.sudo().button_validate()
                    packed_success = True
                except Exception:
                    # 如果驗證失敗，則發送通知到指定頻道
                    channel = self.env['discuss.channel'].sudo().search([('uuid', '=', 'ySHCgPsWJM')], limit=1)
                    record_id = packed_picking.id
                    url = f"/web?#id={record_id}&action=436&model=stock.picking&view_type=form"
                    message_body = Markup(
                        _("❗ Packed quantity transfer failed for <b>%(name)s</b>. Please complete it manually.") % {'name': packed_picking.name}
                        + "<br/>"
                        + _("👉 <a href='%(url)s' target='_blank'>Click here to view</a>") % {'url': url}
                    )
                    channel.sudo().message_post(
                        body=message_body,
                        subject=_("Packed Transfer Failed"),
                        subtype_xmlid='mail.mt_comment',
                        message_type="comment",
                        author_id=2
                    )
                    packed_success = False

            # --- 2. 為免費樣品創建出庫調撥 (Picking)（如果存在樣品）---
            sample_picking = self.env['stock.picking'].sudo()
            sample_success = True  # 初始化為True，因為如果沒有樣品，我們認為它"成功"了
            if total_free_sample_qty > 0:
                # 創建樣品出庫調撥記錄
                sample_picking |= sample_picking.create(self._get_picking_vals(True))

                # 遍歷明細行，為樣品數量創建庫存移動
                for line in self.line_ids:
                    if line.sample_qty == 0:  # 如果冇樣品，則跳過
                        continue

                    total_move_qty_sample = line.sample_qty  # 本次移動嘅總樣品數量
                    if total_move_qty_sample <= 0:
                        continue

                    remaining_sample = total_move_qty_sample  # 剩餘待分配嘅樣品數量

                    sample_picking_list_line_ids = line.package_ids.mapped('picking_list_lines').sorted(lambda x: x.product_id.id)
                    for product_id, g in groupby(sample_picking_list_line_ids, key=lambda x: x.product_id):
                        rounding = product_id.uom_id.rounding
                        if float_is_zero(remaining_sample, precision_rounding=rounding):
                            break
                        s_line_ids = self.env['sb.picking.list.line'].concat(*g)
                        sample_move_total = 0
                        sample_move_line_vals_list = []  # 儲存庫存移動明細行

                        for s_line in s_line_ids:
                            # 已消耗的数量
                            consumed_qty = out_dict.get(s_line, 0)
                            if float_compare(consumed_qty, s_line.quantity, precision_rounding=rounding) != -1:
                                continue

                            available_qty = s_line.quantity - consumed_qty
                            sample_move_line_qty = min([remaining_sample, available_qty])
                            if float_is_zero(sample_move_line_qty, precision_rounding=rounding):
                                break
                            sample_move_line_vals_list.append((0, 0, {
                                # 'quant_id': g_line.id,  # 關聯嘅庫存量
                                'location_id': s_line.quant_id.location_id.id,  # 源位置
                                'location_dest_id': packed_picking.location_dest_id.id,  # 目標位置
                                'product_uom_id': product_id.uom_id.id,  # 產品計量單位
                                'quantity': sample_move_line_qty,  # 完成數量
                                'lot_id': s_line.quant_id.lot_id.id,  # 批次號
                                'product_id': product_id.id,
                            }))

                            sample_move_total += sample_move_line_qty
                            total_move_qty_packed -= sample_move_line_qty

                        if sample_move_line_vals_list:
                            self.env['stock.move'].sudo().create({
                                'name': product_id.display_name + '-FS',  # 移動名稱，區分樣品
                                'product_id': product_id.id,
                                'product_uom_qty': sample_move_total,
                                'product_uom': line.product_id.uom_id.id,
                                'location_id': sample_picking.location_id.id,
                                'location_dest_id': sample_picking.location_dest_id.id,
                                'picking_id': sample_picking.id,
                                'move_line_ids': sample_move_line_vals_list,  # 關聯庫存移動明細行
                                'origin': self.name + ' Samples',
                                # 關鍵：這裡唔設置 sale_line_id，以避免影響銷售訂單數量
                            })

                # 確認並嘗試驗證樣品出庫調撥
                sample_picking.sudo().action_confirm()
                sample_success = False  # 默認設為False，只有驗證成功才設為True
                try:
                    sample_picking.sudo().button_validate()
                    sample_success = True
                except Exception:
                    # 如果驗證失敗，則發送通知到指定頻道
                    channel = self.env['discuss.channel'].sudo().search([('uuid', '=', 'ySHCgPsWJM')], limit=1)
                    record_id = sample_picking.id
                    url = f"/web?#id={record_id}&action=436&model=stock.picking&view_type=form"
                    message_body = Markup(
                        _("❗ Free sample transfer failed for <b>%(name)s</b>. Please complete it manually.") % {'name': sample_picking.name}
                        + "<br/>"
                        + _("👉 <a href='%(url)s' target='_blank'>Click here to view</a>") % {'url': url}
                    )
                    channel.sudo().message_post(
                        body=message_body,
                        subject=_("Sample Transfer Failed"),
                        subtype_xmlid='mail.mt_comment',
                        message_type="comment",
                        author_id=2
                    )
                    sample_success = False

            # 只有當包裝調撥成功且樣品調撥成功時，才更新 PD 狀態為 logistics
            # 如果沒有樣品，sample_success 默認為 True，所以只依賴於 packed_success
            if packed_success and sample_success:
                self.pd_id.state = 'logistics'
                self.sudo().create_invoice()  # 呢個通常係為包裝數量創建發票

            if packed_picking:
                self.instructor_sp = packed_picking.id  # 記錄主調撥（包裝數量）ID
                self.message_post(
                    body=f'Created Outbound Order for Packed Quantity >>> %s' % packed_picking.name,
                    author_id=self.env.user.partner_id.id if not admin else 2,
                )
            if sample_picking:
                self.message_post(
                    body=f'Created Outbound Order for Free Samples >>> %s' % sample_picking.name,
                    author_id=self.env.user.partner_id.id if not admin else 2,
                )

            # 根據是否為管理員操作，返回客戶端通知
            if not admin:
                message_text = _(f'Assignment completed.')
                if packed_picking and sample_picking:
                    message_text = _(f'Both Packed and Sample Outbound Orders created successfully.')
                elif packed_picking:
                    message_text = _(f'Packed Outbound Order created successfully.')
                elif sample_picking:
                    message_text = _(f'Sample Outbound Order created successfully.')

                return {
                    'type': 'ir.actions.client',
                    'tag': 'display_notification',
                    'params': {
                        'title': 'Success',
                        'type': 'success',
                        'message': message_text,
                        'sticky': False,
                        'next': {'type': 'ir.actions.act_window_close'},  # 關閉當前窗口
                    }
                }
        except Exception as e:
            self.env.cr.rollback()  # 發生異常時回滾數據庫事務
            raise ValidationError(e)  # 拋出驗證錯誤

    def _check_repeat(self):
        order_line_ids = [l.sale_order_line_id.id for l in self.line_ids]
        if len(order_line_ids) != len(set(order_line_ids)):
            raise ValidationError(_('The same line appears, check again please.'))
        else:
            return True

    def _check_delivered_state(self, line, order_lines={}):
        """
        order_line: ship.instructions.line(1)
        order_lines: {ship.instructions.line(1): stock.move(12)}
        """
        order_line = line.sale_order_line_id
        for move in order_line.move_ids:
            picking = move.picking_id
            if picking.state in ['done', 'cancel']:
                continue
            else:
                if picking.location_dest_id.usage == 'customer':
                    # 目的位置 —— 客户位置
                    order_lines[line] = move
        return order_lines

    def action_view_sale(self):
        return {
            'type': 'ir.actions.act_window',
            'name': _('Sales'),
            'res_model': 'sale.order',
            'view_mode': 'tree,form',
            'domain': [('id', 'in', self.sale_ids.ids)],
        }

    def action_view_invoices(self):
        """ 打开与当前 Shipping Instructions 关联的发票 """
        self.ensure_one()  # 确保只处理单个记录
        invoices = self.invoice_ids  # 关联的发票
        action = self.env.ref('account.action_move_out_invoice_type').read()[0]  # 获取客户发票的默认 action
        if len(invoices) == 1:
            action.update({
                'views': [(self.env.ref('account.view_move_form').id, 'form')],
                'res_id': invoices.id,
            })
        else:
            action.update({
                'domain': [('id', 'in', invoices.ids)],
            })

        return action

    def action_view_documents(self):
        self.ensure_one()
        return {
            'name': 'Documents',
            'type': 'ir.actions.act_window',
            'res_model': 'documents.document',
            'view_mode': 'kanban,tree,form',
            'domain': [
                ('res_model', '=', 'sb.packing.delivery'),
                ('res_id', '=', self.pd_id.id),
            ],
            'context': {
                'default_res_model': 'sb.packing.delivery',
                'default_res_id': self.pd_id.id,
                'create': False,
                'searchpanel_default_folder_id': self.env.ref('sb_sale.sb_packing_folder').id,
            },
            'target': 'current',
        }

    def print_pick_list(self):
        """print on button click"""
        if not self.pick_lists:
            raise ValidationError("Please create the Package List first.")
        else:
            if self.currency_id.name == 'THB':
                report_external_id = 'sb_sale.sb_delivery_picking_list_report_action'
            else:
                report_external_id = 'sb_sale.sb_picking_list_report_action'
            return self.env.ref(report_external_id).with_context(lang='en_US').report_action(self.id)

    def print_invoice(self):
        """打印多张发票"""
        if not self.invoice_ids:
            raise ValidationError(_("Please create the Invoice first."))
        if self.currency_id.name == 'THB':
            report_external_id = 'sb_sale.sb_inv_picking_list_report_action'
        else:
            report_external_id = 'sb_sale.report_sb_sinv'
        return self.env.ref(report_external_id).with_context(lang='en_US').report_action(self.invoice_ids)
